iT邦幫忙

2026 iThome 鐵人賽

DAY 13
0
Software Development

文藝復興:這段程式碼,好像有點味道系列 第 13

Day 13|租來的畫室,牆上釘不得釘子:不完整的程式庫類別 (Incomplete Library Class)

  • 分享至 

  • xImage
  •  

有些畫室是租來的,牆是房東的牆,樑柱是房東的樑柱,你不能在上面隨便釘釘子、鑿洞掛燈

但畫室要正常運作,總需要掛畫架、放顏料架、裝一盞夠亮的燈

聰明的畫家不會去敲掉房東的牆,而是自己打造一組可以移動的木架,靠在牆邊
需要的功能,全部長在木架上,牆一根釘子都不用動

出貨日要跳過週末

ShippingFeeJob(Day 10)算完運費之後,團隊想加一個「預計送達日」的功能:
從今天起算三個工作天,如果算到週末,要順延到下一個工作天

DateTime 是 .NET 內建的型別,我們沒辦法在它身上加一個 IsWeekend() 方法,它的原始碼不歸我們管

最直覺的做法,是寫一個工具類別:

public static class DateUtils
{
    public static bool IsWeekend(DateTime date) =>
        date.DayOfWeek == DayOfWeek.Saturday || date.DayOfWeek == DayOfWeek.Sunday;

    public static DateTime AddBusinessDays(DateTime date, int days)
    {
        var result = date;
        while (days > 0)
        {
            result = result.AddDays(1);
            if (!IsWeekend(result))
                days--;
        }
        return result;
    }
}

呼叫端用起來是這樣:

var estimatedDelivery = DateUtils.AddBusinessDays(DateTime.Today, 3);

if (DateUtils.IsWeekend(estimatedDelivery))
{
    Console.WriteLine("送達日不應該落在週末");
}

這根釘子,釘在哪裡都不合適

DateUtils 能動,測試也能過,但它帶來幾個隱性代價:

  • DateTime 物件本身,看不出自己還有這些「隱藏能力」
    • 新人拿到一個 DateTime,只能靠 IntelliSense 看到內建方法,根本不會知道專案裡還有一個 DateUtils.IsWeekend() 可以用
  • 呼叫方式,讀起來不太自然
    • DateUtils.IsWeekend(estimatedDelivery),主詞和動作被拆開了,明明是在問「這個日期是不是週末」,語感上卻像是「請 DateUtils 去處理這個日期」
  • 散落的風險
    • 如果另一位工程師不知道 DateUtils 已經存在,很可能會在別的檔案裡,重新寫一個一模一樣的 IsWeekend 判斷

問題不在於寫了一個工具類別,而是把「日期該有的行為」,硬釘在了一面不屬於自己的牆上
牆撐得住,但釘子看起來,永遠是外來的

打造一組屬於自己的活動木架

C# 提供了一個專門為這種情境設計的語法,擴充方法 (Extension Method)

換成畫室的比喻:不動房東的牆,做一組木架,讓它靠在牆邊,看起來就像是牆的一部分

public static class DateTimeExtensions
{
    public static bool IsWeekend(this DateTime date) =>
        date.DayOfWeek == DayOfWeek.Saturday || date.DayOfWeek == DayOfWeek.Sunday;

    public static DateTime AddBusinessDays(this DateTime date, int days)
    {
        var result = date;
        while (days > 0)
        {
            result = result.AddDays(1);
            if (!result.IsWeekend())
                days--;
        }
        return result;
    }
}

關鍵只在參數前面的 this。加了這個字,IsWeekend 就像是長在 DateTime 身上的原生方法

呼叫端變成:

var estimatedDelivery = DateTime.Today.AddBusinessDays(3);

if (estimatedDelivery.IsWeekend())
{
    Console.WriteLine("送達日不應該落在週末");
}

讀起來,跟呼叫 DateTime 原生的 AddDays() 沒有任何差別

新人打出 estimatedDelivery. 的時候,IntelliSense 會自動列出 IsWeekend()AddBusinessDays(),就像它們一直都在那裡一樣

小補丁用擴充方法,大改造要換個做法

不是所有「函式庫不夠用」的情況,擴充方法都能解決

  • 只是缺少幾個小方法
    • 擴充方法是最輕量、最自然的做法,DateTime 這個案例就很適合
  • 需要大幅改變互動方式,例如要在每次呼叫前後加驗證、記錄、快取邏輯
    • 這種情況更適合建立一個包裝類別,把原始物件包在裡面,只公開你真正需要的介面,本質上就是轉接器模式

記得,這根木架是有維護成本的

擴充方法讓生活變輕鬆,但它不是沒有代價:

如果哪天函式庫升級,內部行為改變了,你的擴充方法可能悄悄跟著失準,卻不會有任何編譯錯誤提醒你

多加的每一個擴充方法,都是團隊要長期維護的資產,不是免費的午餐

自我檢查清單

  1. 我是不是正在為一個第三方或框架提供的類別,寫一個「外部工具方法」?
  2. 這個工具方法的第一個參數,是不是總是同一個函式庫物件?
  3. 呼叫這個工具方法時,讀起來是「物件在做某件事」,還是「把物件丟給某個地方處理」?
  4. 這個功能,換成 C# 的擴充方法,會不會讓呼叫端讀起來更自然?
  5. 如果函式庫日後升級,我有沒有把這個擴充方法的假設寫清楚,方便未來檢查是否還成立?

明日預告

模組一蓋起了圓頂,模組二找到了透視法,明天我們要去看西斯汀教堂的天頂

米開朗基羅在灰泥乾掉之前,只有幾個小時可以決定筆觸。
時間壓力下的程式碼,會長出另一種完全不同的壞味道

模組三,正式開工


上一篇
Day 12|兩位畫家,同一個場景,不同的簽名方式:異曲同工的類別 (Alternative Classes with Different Interfaces)
下一篇
Day 14|達文西退後三步:結構對了,還要整幅畫都對得上
系列文
文藝復興:這段程式碼,好像有點味道27
圖片
  熱門推薦
圖片
{{ item.channelVendor }} | {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

尚未有邦友留言

立即登入留言